/**
 * @license
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Contains the controller code for workspace factory. Depends
 * on the model and view objects (created as internal variables) and interacts
 * with previewWorkspace and toolboxWorkspace (internal references stored to
 * both). Also depends on standard_categories.js for standard Blockly
 * categories. Provides the functionality for the actions the user can initiate:
 * - adding and removing categories
 * - switching between categories
 * - printing and downloading configuration xml
 * - updating the preview workspace
 * - changing a category name
 * - moving the position of a category.
 *
 * @author Emma Dauterman (evd2014)
 */

/**
 * Xiaohong XeLa formatting document
 * Xiaohong XeLa Chinese document
 * Xiaohong XeLa modify many places
 */

/**
 * Class for a WorkspaceFactoryController
 * @param {string} toolboxName Name of workspace toolbox XML.
 * @param {string} toolboxDiv Name of div to inject toolbox workspace in.
 * @param {string} previewDiv Name of div to inject preview workspace in.
 * @constructor
 */
WorkspaceFactoryController = function (toolboxName, toolboxDiv, previewDiv) {
    // Toolbox XML element for the editing workspace.
    this.toolbox = document.getElementById(toolboxName);

    // Workspace for user to drag blocks in for a certain category.
    this.toolboxWorkspace = Blockly.inject(toolboxDiv,
        {
            grid:
            {
                spacing: 25,
                length: 3,
                colour: '#ccc',
                snap: true
            },
            media: '../../media/',
            toolbox: this.toolbox,
            zoom: {
                // 缩放设置
                controls: true, // 显示缩放按钮控件
                wheel: true, // 允许使用鼠标滚轮缩放
                startScale: 1, // 初始积木大小
                maxScale: 3, // 最大大小
                minScale: 0.3, // 最小大小
            },
            move: {
                // 移动设置
                wheel: true, // 允许鼠标滚轮滑动
            },
            renderer: "zelos",
        });

    // Workspace for user to preview their changes.
    this.previewWorkspace = Blockly.inject(previewDiv,
        {
            grid:
            {
                spacing: 25,
                length: 3,
                colour: '#ccc',
                snap: true
            },
            media: '../../media/',
            toolbox: '<xml xmlns="https://developers.google.com/blockly/xml"></xml>',
            renderer: "zelos",
            zoom: {
                // 缩放设置
                controls: true, // 显示缩放按钮控件
                wheel: true, // 允许使用鼠标滚轮缩放
                startScale: 1, // 初始积木大小
                maxScale: 3, // 最大大小
                minScale: 0.3, // 最小大小
            },
            move: {
                // 移动设置
                wheel: true, // 允许鼠标滚轮滑动
            },
        });

    // Model to keep track of categories and blocks.
    this.model = new WorkspaceFactoryModel();
    // Updates the category tabs.
    this.view = new WorkspaceFactoryView();
    // Generates XML for categories.
    this.generator = new WorkspaceFactoryGenerator(this.model);
    // Tracks which editing mode the user is in. Toolbox mode on start.
    this.selectedMode = WorkspaceFactoryController.MODE_TOOLBOX;
    // True if key events are enabled, false otherwise.
    this.keyEventsEnabled = true;
    // True if there are unsaved changes in the toolbox, false otherwise.
    this.hasUnsavedToolboxChanges = false;
    // True if there are unsaved changes in the preloaded blocks, false otherwise.
    this.hasUnsavedPreloadChanges = false;
};

// Toolbox editing mode. Changes the user makes to the workspace updates the
// toolbox.
WorkspaceFactoryController.MODE_TOOLBOX = 'toolbox';
// Pre-loaded workspace editing mode. Changes the user makes to the workspace
// udpates the pre-loaded blocks.
WorkspaceFactoryController.MODE_PRELOAD = 'preload';

/**
 * Currently prompts the user for a name, checking that it's valid (not used
 * before), and then creates a tab and switches to it.
 */
WorkspaceFactoryController.prototype.addCategory = function () {
    // Transfers the user's blocks to a flyout if it's the first category created.
    this.transferFlyoutBlocksToCategory();

    // After possibly creating a category, check again if it's the first category.
    var isFirstCategory = !this.model.hasElements();
    // Get name from user.
    var name = this.promptForNewCategoryName('输入新类别的名称:');
    if (!name) {  // Exit if cancelled.
        return;
    }
    // Create category.
    this.createCategory(name);
    // Switch to category.
    this.switchElement(this.model.getCategoryIdByName(name));

    // Sets the default options for injecting the workspace
    // when there are categories if adding the first category.
    if (isFirstCategory) {
        this.view.setCategoryOptions(this.model.hasElements());
        this.generateNewOptions();
    }
    // Update preview.
    this.updatePreview();
};

/**
 * Helper method for addCategory. Adds a category to the view given a name, ID,
 * and a boolean for if it's the first category created. Assumes the category
 * has already been created in the model. Does not switch to category.
 * @param {string} name Name of category being added.
 * @param {string} id The ID of the category being added.
 */
WorkspaceFactoryController.prototype.createCategory = function (name) {
    // Create empty category
    var category = new ListElement(ListElement.TYPE_CATEGORY, name);
    this.model.addElementToList(category);
    // Create new category.
    var tab = this.view.addCategoryRow(name, category.id);
    this.addClickToSwitch(tab, category.id);
};

/**
 * Given a tab and a ID to be associated to that tab, adds a listener to
 * that tab so that when the user clicks on the tab, it switches to the
 * element associated with that ID.
 * @param {!Element} tab The DOM element to add the listener to.
 * @param {string} id The ID of the element to switch to when tab is clicked.
 */
WorkspaceFactoryController.prototype.addClickToSwitch = function (tab, id) {
    var self = this;
    var clickFunction = function (id) {  // Keep this in scope for switchElement.
        return function () {
            self.switchElement(id);
        };
    };
    this.view.bindClick(tab, clickFunction(id));
};

/**
 * Transfers the blocks in the user's flyout to a new category if
 * the user is creating their first category and their workspace is not
 * empty. Should be called whenever it is possible to switch from single flyout
 * to categories (not including importing).
 */
WorkspaceFactoryController.prototype.transferFlyoutBlocksToCategory =
    function () {
        // Saves the user's blocks from the flyout in a category if there is no
        // toolbox and the user has dragged in blocks.
        if (!this.model.hasElements() &&
            this.toolboxWorkspace.getAllBlocks(false).length > 0) {
            // Create the new category.
            this.createCategory('Category 1', true);
            // Set the new category as selected.
            var id = this.model.getCategoryIdByName('Category 1');
            this.model.setSelectedById(id);
            this.view.setCategoryTabSelection(id, true);
            // Allow user to use the default options for injecting with categories.
            this.view.setCategoryOptions(this.model.hasElements());
            this.generateNewOptions();
            // Update preview here in case exit early.
            this.updatePreview();
        }
    };

/**
 * Attached to "-" button. Checks if the user wants to delete
 * the current element.  Removes the element and switches to another element.
 * When the last element is removed, it switches to a single flyout mode.
 */
WorkspaceFactoryController.prototype.removeElement = function () {
    // Check that there is a currently selected category to remove.
    if (!this.model.getSelected()) {
        return;
    }

    // Check if user wants to remove current category.
    var check = confirm('您确定要删除当前选择的 '
        + this.model.getSelected().type + ' 吗？');
    if (!check) { // If cancelled, exit.
        return;
    }

    var selectedId = this.model.getSelectedId();
    var selectedIndex = this.model.getIndexByElementId(selectedId);
    // Delete element visually.
    this.view.deleteElementRow(selectedId, selectedIndex);
    // Delete element in model.
    this.model.deleteElementFromList(selectedIndex);

    // Find next logical element to switch to.
    var next = this.model.getElementByIndex(selectedIndex);
    if (!next && this.model.hasElements()) {
        next = this.model.getElementByIndex(selectedIndex - 1);
    }
    var nextId = next ? next.id : null;

    // Open next element.
    this.clearAndLoadElement(nextId);

    // If no element to switch to, display message, clear the workspace, and
    // set a default selected element not in toolbox list in the model.
    if (!nextId) {
        alert('您目前没有类别或分隔符。所有积木都将显示在一个弹出按钮中。');
        this.toolboxWorkspace.clear();
        this.toolboxWorkspace.clearUndo();
        this.model.createDefaultSelectedIfEmpty();
    }
    // Update preview.
    this.updatePreview();
};

/**
 * Gets a valid name for a new category from the user.
 * @param {string} promptString Prompt for the user to enter a name.
 * @param {string=} opt_oldName The current name.
 * @return {string?} Valid name for a new category, or null if cancelled.
 */
WorkspaceFactoryController.prototype.promptForNewCategoryName =
    function (promptString, opt_oldName) {
        var defaultName = opt_oldName;
        do {
            var name = prompt(promptString, defaultName);
            if (!name) {  // If cancelled.
                return null;
            }
            defaultName = name;
        } while (this.model.hasCategoryByName(name));
        return name;
    };

/**
 * Switches to a new tab for the element given by ID. Stores XML and blocks
 * to reload later, updates selected accordingly, and clears the workspace
 * and clears undo, then loads the new element.
 * @param {string} id ID of tab to be opened, must be valid element ID.
 */
WorkspaceFactoryController.prototype.switchElement = function (id) {
    // Disables events while switching so that Blockly delete and create events
    // don't update the preview repeatedly.
    Blockly.Events.disable();
    // Caches information to reload or generate XML if switching to/from element.
    // Only saves if a category is selected.
    if (this.model.getSelectedId() != null && id != null) {
        this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
    }
    // Load element.
    this.clearAndLoadElement(id);
    // Enable Blockly events again.
    Blockly.Events.enable();
};

/**
 * Switches to a new tab for the element by ID. Helper for switchElement.
 * Updates selected, clears the workspace and clears undo, loads a new element.
 * @param {string} id ID of category to load.
 */
WorkspaceFactoryController.prototype.clearAndLoadElement = function (id) {
    // Unselect current tab if switching to and from an element.
    if (this.model.getSelectedId() != null && id != null) {
        this.view.setCategoryTabSelection(this.model.getSelectedId(), false);
    }

    // If switching to another category, set category selection in the model and
    // view.
    if (id != null) {
        // Set next category.
        this.model.setSelectedById(id);

        // Clears workspace and loads next category.
        this.clearAndLoadXml_(this.model.getSelectedXml());

        // Selects the next tab.
        this.view.setCategoryTabSelection(id, true);

        // Order blocks as shown in flyout.
        this.toolboxWorkspace.cleanUp();

        // Update category editing buttons.
        this.view.updateState(this.model.getIndexByElementId
            (this.model.getSelectedId()), this.model.getSelected());
    } else {
        // Update category editing buttons for no categories.
        this.view.updateState(-1, null);
    }
};

/**
 * Tied to "Export" button. Gets a file name from the user and downloads
 * the corresponding configuration XML to that file.
 * @param {string} exportMode The type of file to export
 *    (WorkspaceFactoryController.MODE_TOOLBOX for the toolbox configuration,
 *    and WorkspaceFactoryController.MODE_PRELOAD for the pre-loaded workspace
 *    configuration)
 */
WorkspaceFactoryController.prototype.exportXmlFile = function (exportMode) {
    // Get file name.
    if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) {
        var fileName = prompt('工具箱 XML 的文件名:', 'toolbox.xml');
    } else {
        var fileName = prompt('预加载工作空间 XML 的文件名:',
            'workspace.xml');
    }
    if (!fileName) {  // If cancelled.
        return;
    }

    // Generate XML.
    if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) {
        // Export the toolbox XML.
        var configXml = Blockly.Xml.domToPrettyText(
            this.generator.generateToolboxXml());
        this.hasUnsavedToolboxChanges = false;
    } else if (exportMode == WorkspaceFactoryController.MODE_PRELOAD) {
        // Export the pre-loaded block XML.
        var configXml = Blockly.Xml.domToPrettyText(
            this.generator.generateWorkspaceXml());
        this.hasUnsavedPreloadChanges = false;
    } else {
        // Unknown mode. Throw error.
        var msg = '未知的导出模式: ' + exportMode;
        BlocklyDevTools.Analytics.onError(msg);
        throw Error(msg);
    }

    // Download file.
    var data = new Blob([configXml], { type: 'text/xml' });
    this.view.createAndDownloadFile(fileName, data);

    if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) {
        BlocklyDevTools.Analytics.onExport(
            BlocklyDevTools.Analytics.TOOLBOX,
            { format: BlocklyDevTools.Analytics.FORMAT_XML });
    } else if (exportMode == WorkspaceFactoryController.MODE_PRELOAD) {
        BlocklyDevTools.Analytics.onExport(
            BlocklyDevTools.Analytics.WORKSPACE_CONTENTS,
            { format: BlocklyDevTools.Analytics.FORMAT_XML });
    }
};

/**
 * Export the options object to be used for the Blockly inject call. Gets a
 * file name from the user and downloads the options object to that file.
 */
WorkspaceFactoryController.prototype.exportInjectFile = function () {
    var fileName = prompt('Blockly工作区配置代码的文件名:',
        'workspace.js');
    if (!fileName) {  // If cancelled.
        return;
    }
    // Generate new options to remove toolbox XML from options object (if
    // necessary).
    this.generateNewOptions();
    var printableOptions = this.generator.generateInjectString()
    var data = new Blob([printableOptions], { type: 'text/javascript' });
    this.view.createAndDownloadFile(fileName, data);

    BlocklyDevTools.Analytics.onExport(
        BlocklyDevTools.Analytics.STARTER_CODE,
        {
            format: BlocklyDevTools.Analytics.FORMAT_JS,
            platform: BlocklyDevTools.Analytics.PLATFORM_WEB
        });
};

/**
 * Tied to "Print" button. Mainly used for debugging purposes. Prints
 * the configuration XML to the console.
 */
WorkspaceFactoryController.prototype.printConfig = function () {
    // Capture any changes made by user before generating XML.
    this.saveStateFromWorkspace();
    // Print XML.
    console.log(Blockly.Xml.domToPrettyText(this.generator.generateToolboxXml()));
};

/**
 * Updates the preview workspace based on the toolbox workspace. If switching
 * from no categories to categories or categories to no categories, reinjects
 * Blockly with reinjectPreview, otherwise just updates without reinjecting.
 * Called whenever a list element is created, removed, or modified and when
 * Blockly move and delete events are fired. Do not call on create events
 * or disabling will cause the user to "drop" their current blocks. Make sure
 * that no changes have been made to the workspace since updating the model
 * (if this might be the case, call saveStateFromWorkspace).
 */
WorkspaceFactoryController.prototype.updatePreview = function () {
    // Disable events to stop updatePreview from recursively calling itself
    // through event handlers.
    Blockly.Events.disable();

    // Only update the toolbox if not in read only mode.
    if (!this.model.options['readOnly']) {
        // Get toolbox XML.
        var tree = Blockly.utils.toolbox.parseToolboxTree(
            this.generator.generateToolboxXml());

        // No categories, creates a simple flyout.
        if (tree.getElementsByTagName('category').length == 0) {
            // No categories, creates a simple flyout.
            if (this.previewWorkspace.toolbox_) {
                this.reinjectPreview(tree); // Switch to simple flyout, expensive.
            } else {
                this.previewWorkspace.updateToolbox(tree);
            }
        } else {
            // Uses categories, creates a toolbox.
            if (!this.previewWorkspace.toolbox_) {
                this.reinjectPreview(tree); // Create a toolbox, expensive.
            } else {
                // Close the toolbox before updating it so that the user has to reopen
                // the flyout and see their updated toolbox (open flyout doesn't update)
                this.previewWorkspace.toolbox_.clearSelection();
                this.previewWorkspace.updateToolbox(tree);
            }
        }
    }

    // Update pre-loaded blocks in the preview workspace.
    this.previewWorkspace.clear();
    Blockly.Xml.domToWorkspace(this.generator.generateWorkspaceXml(),
        this.previewWorkspace);

    // Reenable events.
    Blockly.Events.enable();
};

/**
 * Saves the state from the workspace depending on the current mode. Should
 * be called after making changes to the workspace.
 */
WorkspaceFactoryController.prototype.saveStateFromWorkspace = function () {
    if (this.selectedMode == WorkspaceFactoryController.MODE_TOOLBOX) {
        // If currently editing the toolbox.
        // Update flags if toolbox has been changed.
        if (this.model.getSelectedXml() !=
            Blockly.Xml.workspaceToDom(this.toolboxWorkspace)) {
            this.hasUnsavedToolboxChanges = true;
        }

        this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);

    } else if (this.selectedMode == WorkspaceFactoryController.MODE_PRELOAD) {
        // If currently editing the pre-loaded workspace.
        // Update flags if preloaded blocks have been changed.
        if (this.model.getPreloadXml() !=
            Blockly.Xml.workspaceToDom(this.toolboxWorkspace)) {
            this.hasUnsavedPreloadChanges = true;
        }

        this.model.savePreloadXml(
            Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
    }
};

/**
 * Used to completely reinject the preview workspace. This should be used only
 * when switching from simple flyout to categories, or categories to simple
 * flyout. More expensive than simply updating the flyout or toolbox.
 * @param {!Element} Tree of XML elements
 * @package
 */
WorkspaceFactoryController.prototype.reinjectPreview = function (tree) {
    this.previewWorkspace.dispose();
    var injectOptions = this.readOptions_();
    injectOptions['toolbox'] = Blockly.Xml.domToPrettyText(tree);
    injectOptions["renderer"] = "zelos"
    this.previewWorkspace = Blockly.inject('preview_blocks', injectOptions);
    Blockly.Xml.domToWorkspace(this.generator.generateWorkspaceXml(),
        this.previewWorkspace);
};

/**
 * Changes the name and colour of the selected category.
 * Return if selected element is a separator.
 * @param {string} name New name for selected category.
 * @param {?string} colour New colour for selected category, or null if none.
 * Must be a valid CSS string, or '' for none.
 */
WorkspaceFactoryController.prototype.changeSelectedCategory = function (name,
    colour) {
    var selected = this.model.getSelected();
    // Return if a category is not selected.
    if (selected.type != ListElement.TYPE_CATEGORY) {
        return;
    }
    // Change colour of selected category.
    selected.changeColor(colour);
    this.view.setBorderColor(this.model.getSelectedId(), colour);
    // Change category name.
    selected.changeName(name);
    this.view.updateCategoryName(name, this.model.getSelectedId());
    // Update preview.
    this.updatePreview();
};

/**
 * Tied to arrow up and arrow down buttons. Swaps with the element above or
 * below the currently selected element (offset categories away from the
 * current element). Updates state to enable the correct element editing
 * buttons.
 * @param {number} offset The index offset from the currently selected element
 * to swap with. Positive if the element to be swapped with is below, negative
 * if the element to be swapped with is above.
 */
WorkspaceFactoryController.prototype.moveElement = function (offset) {
    var curr = this.model.getSelected();
    if (!curr) {  // Return if no selected element.
        return;
    }
    var currIndex = this.model.getIndexByElementId(curr.id);
    var swapIndex = this.model.getIndexByElementId(curr.id) + offset;
    var swap = this.model.getElementByIndex(swapIndex);
    if (!swap) {  // Return if cannot swap in that direction.
        return;
    }
    // Move currently selected element to index of other element.
    // Indexes must be valid because confirmed that curr and swap exist.
    this.moveElementToIndex(curr, swapIndex, currIndex);
    // Update element editing buttons.
    this.view.updateState(swapIndex, this.model.getSelected());
    // Update preview.
    this.updatePreview();
};

/**
 * Moves a element to a specified index and updates the model and view
 * accordingly. Helper functions throw an error if indexes are out of bounds.
 * @param {!Element} element The element to move.
 * @param {number} newIndex The index to insert the element at.
 * @param {number} oldIndex The index the element is currently at.
 */
WorkspaceFactoryController.prototype.moveElementToIndex = function (element,
    newIndex, oldIndex) {
    this.model.moveElementToIndex(element, newIndex, oldIndex);
    this.view.moveTabToIndex(element.id, newIndex, oldIndex);
};

/**
 * Tied to the "Standard Category" dropdown option, this function prompts
 * the user for a name of a standard Blockly category (case insensitive) and
 * loads it as a new category and switches to it. Leverages StandardCategories.
 */
WorkspaceFactoryController.prototype.loadCategory = function () {
    // Prompt user for the name of the standard category to load.
    do {
        var name = prompt('输入要导入的类别名称：\nLogic, Loops, Math, Text, Lists, Colour, Variables, TypedVariables or Functions');
        if (!name) {
            return;  // Exit if cancelled.
        }
    } while (!this.isStandardCategoryName(name));

    // Load category.
    this.loadCategoryByName(name);
};

/**
 * Loads a Standard Category by name and switches to it. Leverages
 * StandardCategories. Returns if cannot load standard category.
 * @param {string} name Name of the standard category to load.
 */
WorkspaceFactoryController.prototype.loadCategoryByName = function (name) {
    // Check if the user can load that standard category.
    if (!this.isStandardCategoryName(name)) {
        return;
    }
    if (this.model.hasVariables() && name.toLowerCase() == 'variables') {
        alert('变量类别已经存在。您不能创建多个变量类别。');
        return;
    }
    if (this.model.hasProcedures() && name.toLowerCase() == 'functions') {
        alert('功能类别已经存在。您不能创建多个功能类别。');
        return;
    }
    // Check if the user can create a category with that name.
    var standardCategory = StandardCategories.categoryMap[name.toLowerCase()]
    if (this.model.hasCategoryByName(standardCategory.name)) {
        alert('您已经有一个名为 ' + standardCategory.name
            + ' 的类别、请重命名您的类别，然后重试。');
        return;
    }
    if (!standardCategory.color && standardCategory.hue !== undefined) {
        // Calculate the hex colour based on the hue.
        standardCategory.color = Blockly.hueToHex(standardCategory.hue);
    }
    // Transfers current flyout blocks to a category if it's the first category
    // created.
    this.transferFlyoutBlocksToCategory();

    var isFirstCategory = !this.model.hasElements();
    // Copy the standard category in the model.
    var copy = standardCategory.copy();

    // Add it to the model.
    this.model.addElementToList(copy);

    // Update the copy in the view.
    var tab = this.view.addCategoryRow(copy.name, copy.id);
    this.addClickToSwitch(tab, copy.id);
    // Color the category tab in the view.
    if (copy.color) {
        this.view.setBorderColor(copy.id, copy.color);
    }
    // Switch to loaded category.
    this.switchElement(copy.id);
    // Convert actual shadow blocks to user-generated shadow blocks.
    this.convertShadowBlocks();
    // Save state from workspace before updating preview.
    this.saveStateFromWorkspace();
    if (isFirstCategory) {
        // Allow the user to use the default options for injecting the workspace
        // when there are categories.
        this.view.setCategoryOptions(this.model.hasElements());
        this.generateNewOptions();
    }
    // Update preview.
    this.updatePreview();
};

/**
 * Loads the standard Blockly toolbox into the editing space. Should only
 * be called when the mode is set to toolbox.
 */
WorkspaceFactoryController.prototype.loadStandardToolbox = function () {
    this.loadCategoryByName('Logic');
    this.loadCategoryByName('Loops');
    this.loadCategoryByName('Math');
    this.loadCategoryByName('Text');
    this.loadCategoryByName('Lists');
    this.loadCategoryByName('Colour');
    this.addSeparator();
    this.loadCategoryByName('Variables');
    this.loadCategoryByName('Functions');
};

/**
 * Given the name of a category, determines if it's the name of a standard
 * category (case insensitive).
 * @param {string} name The name of the category that should be checked if it's
 * in StandardCategories categoryMap
 * @return {boolean} True if name is a standard category name, false otherwise.
 */
WorkspaceFactoryController.prototype.isStandardCategoryName = function (name) {
    return !!StandardCategories.categoryMap[name.toLowerCase()];
};

/**
 * Connected to the "add separator" dropdown option. If categories already
 * exist, adds a separator to the model and view. Does not switch to select
 * the separator, and updates the preview.
 */
WorkspaceFactoryController.prototype.addSeparator = function () {
    // If adding the first element in the toolbox, transfers the user's blocks
    // in a flyout to a category.
    this.transferFlyoutBlocksToCategory();
    // Create the separator in the model.
    var separator = new ListElement(ListElement.TYPE_SEPARATOR);
    this.model.addElementToList(separator);
    // Create the separator in the view.
    var tab = this.view.addSeparatorTab(separator.id);
    this.addClickToSwitch(tab, separator.id);
    // Switch to the separator and update the preview.
    this.switchElement(separator.id);
    this.updatePreview();
};

/**
 * Connected to the import button. Given the file path inputted by the user
 * from file input, if the import mode is for the toolbox, this function loads
 * that toolbox XML to the workspace, creating category and separator tabs as
 * necessary. If the import mode is for pre-loaded blocks in the workspace,
 * this function loads that XML to the workspace to be edited further. This
 * function switches mode to whatever the import mode is. Catches errors from
 * file reading and prints an error message alerting the user.
 * @param {string} file The path for the file to be imported into the workspace.
 *   Should contain valid toolbox XML.
 * @param {string} importMode The mode corresponding to the type of file the
 *   user is importing (WorkspaceFactoryController.MODE_TOOLBOX or
 *   WorkspaceFactoryController.MODE_PRELOAD).
 */
WorkspaceFactoryController.prototype.importFile = function (file, importMode) {
    // Exit if cancelled.
    if (!file) {
        return;
    }

    Blockly.Events.disable();
    var controller = this;
    var reader = new FileReader();

    // To be executed when the reader has read the file.
    reader.onload = function () {
        // Try to parse XML from file and load it into toolbox editing area.
        // Print error message if fail.
        try {
            var tree = Blockly.Xml.textToDom(reader.result);
            if (importMode == WorkspaceFactoryController.MODE_TOOLBOX) {
                // Switch mode.
                controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX);

                // Confirm that the user wants to override their current toolbox.
                var hasToolboxElements = controller.model.hasElements() ||
                    controller.toolboxWorkspace.getAllBlocks(false).length > 0;
                if (hasToolboxElements) {
                    var msg = '您确定要导入吗？您将丢失当前的工具箱。';
                    BlocklyDevTools.Analytics.onWarning(msg);
                    var continueAnyway = confirm();
                    if (!continueAnyway) {
                        return;
                    }
                }
                // Import toolbox XML.
                controller.importToolboxFromTree_(tree);
                BlocklyDevTools.Analytics.onImport('Toolbox.xml');

            } else if (importMode == WorkspaceFactoryController.MODE_PRELOAD) {
                // Switch mode.
                controller.setMode(WorkspaceFactoryController.MODE_PRELOAD);

                // Confirm that the user wants to override their current blocks.
                if (controller.toolboxWorkspace.getAllBlocks(false).length > 0) {
                    var msg = '您确定要导入吗？您将丢失当前的工作区积木。';
                    var continueAnyway = confirm(msg);
                    BlocklyDevTools.Analytics.onWarning(msg);
                    if (!continueAnyway) {
                        return;
                    }
                }

                // Import pre-loaded workspace XML.
                controller.importPreloadFromTree_(tree);
                BlocklyDevTools.Analytics.onImport('WorkspaceContents.xml');
            } else {
                // Throw error if invalid mode.
                throw Error('Unknown import mode: ' + importMode);
            }
        } catch (e) {
            var msg = '无法从文件加载XML。';
            alert(msg);
            BlocklyDevTools.Analytics.onError(msg);
            console.log(e);
        } finally {
            Blockly.Events.enable();
        }
    }

    // Read the file asynchronously.
    reader.readAsText(file);
};

/**
 * Given a XML DOM tree, loads it into the toolbox editing area so that the
 * user can continue editing their work. Assumes that tree is in valid toolbox
 * XML format. Assumes that the mode is MODE_TOOLBOX.
 * @param {!Element} tree XML tree to be loaded to toolbox editing area.
 * @private
 */
WorkspaceFactoryController.prototype.importToolboxFromTree_ = function (tree) {
    // Clear current editing area.
    this.model.clearToolboxList();
    this.view.clearToolboxTabs();

    if (tree.getElementsByTagName('category').length == 0) {
        // No categories present.
        // Load all the blocks into a single category evenly spaced.
        Blockly.Xml.domToWorkspace(tree, this.toolboxWorkspace);
        this.toolboxWorkspace.cleanUp();

        // Convert actual shadow blocks to user-generated shadow blocks.
        this.convertShadowBlocks();

        // Add message to denote empty category.
        this.view.addEmptyCategoryMessage();

    } else {
        // Categories/separators present.
        for (var i = 0, item; item = tree.children[i]; i++) {

            if (item.tagName == 'category') {
                // If the element is a category, create a new category and switch to it.
                this.createCategory(item.getAttribute('name'), false);
                var category = this.model.getElementByIndex(i);
                this.switchElement(category.id);

                // Load all blocks in that category to the workspace to be evenly
                // spaced and saved to that category.
                for (var j = 0, blockXml; blockXml = item.children[j]; j++) {
                    Blockly.Xml.domToBlock(blockXml, this.toolboxWorkspace);
                }

                // Evenly space the blocks.
                this.toolboxWorkspace.cleanUp();

                // Convert actual shadow blocks to user-generated shadow blocks.
                this.convertShadowBlocks();

                // Set category color.
                if (item.getAttribute('colour')) {
                    category.changeColor(item.getAttribute('colour'));
                    this.view.setBorderColor(category.id, category.color);
                }
                // Set any custom tags.
                if (item.getAttribute('custom')) {
                    this.model.addCustomTag(category, item.getAttribute('custom'));
                }
            } else {
                // If the element is a separator, add the separator and switch to it.
                this.addSeparator();
                this.switchElement(this.model.getElementByIndex(i).id);
            }
        }
    }
    this.view.updateState(this.model.getIndexByElementId
        (this.model.getSelectedId()), this.model.getSelected());

    this.saveStateFromWorkspace();

    // Set default configuration options for a single flyout or multiple
    // categories.
    this.view.setCategoryOptions(this.model.hasElements());
    this.generateNewOptions();

    this.updatePreview();
};

/**
 * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
 * Assumes that tree is in valid XML format and that the selected mode is
 * MODE_PRELOAD.
 * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
 *   area.
 */
WorkspaceFactoryController.prototype.importPreloadFromTree_ = function (tree) {
    this.clearAndLoadXml_(tree);
    this.model.savePreloadXml(tree);
    this.updatePreview();
};

/**
 * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
 * Assumes that tree is in valid XML format and that the selected mode is
 * MODE_PRELOAD.
 * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
 *   area.
 */
WorkspaceFactoryController.prototype.importPreloadFromTree_ = function (tree) {
    this.clearAndLoadXml_(tree);
    this.model.savePreloadXml(tree);
    this.saveStateFromWorkspace();
    this.updatePreview();
};

/**
 * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
 * Assumes that tree is in valid XML format and that the selected mode is
 * MODE_PRELOAD.
 * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
 *   area.
 */
WorkspaceFactoryController.prototype.importPreloadFromTree_ = function (tree) {
    this.clearAndLoadXml_(tree);
    this.model.savePreloadXml(tree);
    this.saveStateFromWorkspace();
    this.updatePreview();
};

/**
 * Clears the editing area completely, deleting all categories and all
 * blocks in the model and view and all pre-loaded blocks. Tied to the
 * "Clear" button.
 */
WorkspaceFactoryController.prototype.clearAll = function () {
    var msg = '是否确定要清除工作区工厂中的所有工作？';
    BlocklyDevTools.Analytics.onWarning(msg);
    if (!confirm(msg)) {
        return;
    }
    var hasCategories = this.model.hasElements();
    this.model.clearToolboxList();
    this.view.clearToolboxTabs();
    this.model.savePreloadXml(Blockly.utils.xml.createElement('xml'));
    this.view.addEmptyCategoryMessage();
    this.view.updateState(-1, null);
    this.toolboxWorkspace.clear();
    this.toolboxWorkspace.clearUndo();
    this.saveStateFromWorkspace();
    this.hasUnsavedToolboxChanges = false;
    this.hasUnsavedPreloadChanges = false;
    this.view.setCategoryOptions(this.model.hasElements());
    this.generateNewOptions();
    this.updatePreview();
};

/**
 * Makes the currently selected block a user-generated shadow block. These
 * blocks are not made into real shadow blocks, but recorded in the model
 * and visually marked as shadow blocks, allowing the user to move and edit
 * them (which would be impossible with actual shadow blocks). Updates the
 * preview when done.
 */
WorkspaceFactoryController.prototype.addShadow = function () {
    // No block selected to make a shadow block.
    if (!Blockly.selected) {
        return;
    }
    // Clear any previous warnings on the block (would only have warnings on
    // a non-shadow block if it was nested inside another shadow block).
    Blockly.selected.setWarningText(null);
    // Set selected block and all children as shadow blocks.
    this.addShadowForBlockAndChildren_(Blockly.selected);

    // Save and update the preview.
    this.saveStateFromWorkspace();
    this.updatePreview();
};

/**
 * Sets a block and all of its children to be user-generated shadow blocks,
 * both in the model and view.
 * @param {!Blockly.Block} block The block to be converted to a user-generated
 *    shadow block.
 * @private
 */
WorkspaceFactoryController.prototype.addShadowForBlockAndChildren_ =
    function (block) {
        // Convert to shadow block.
        this.view.markShadowBlock(block);
        this.model.addShadowBlock(block.id);

        if (FactoryUtils.hasVariableField(block)) {
            block.setWarningText('不能使变量积木成为阴影积木。');
        }

        // Convert all children to shadow blocks recursively.
        var children = block.getChildren();
        for (var i = 0; i < children.length; i++) {
            this.addShadowForBlockAndChildren_(children[i]);
        }
    };

/**
 * If the currently selected block is a user-generated shadow block, this
 * function makes it a normal block again, removing it from the list of
 * shadow blocks and loading the workspace again. Updates the preview again.
 */
WorkspaceFactoryController.prototype.removeShadow = function () {
    // No block selected to modify.
    if (!Blockly.selected) {
        return;
    }
    this.model.removeShadowBlock(Blockly.selected.id);
    this.view.unmarkShadowBlock(Blockly.selected);

    // If turning invalid shadow block back to normal block, remove warning.
    Blockly.selected.setWarningText(null);

    this.saveStateFromWorkspace();
    this.updatePreview();
};

/**
 * Given a unique block ID, uses the model to determine if a block is a
 * user-generated shadow block.
 * @param {string} blockId The unique ID of the block to examine.
 * @return {boolean} True if the block is a user-generated shadow block, false
 *    otherwise.
 */
WorkspaceFactoryController.prototype.isUserGenShadowBlock = function (blockId) {
    return this.model.isShadowBlock(blockId);
};

/**
 * Call when importing XML containing real shadow blocks. This function turns
 * all real shadow blocks loaded in the workspace into user-generated shadow
 * blocks, meaning they are marked as shadow blocks by the model and appear as
 * shadow blocks in the view but are still editable and movable.
 */
WorkspaceFactoryController.prototype.convertShadowBlocks = function () {
    var blocks = this.toolboxWorkspace.getAllBlocks(false);
    for (var i = 0, block; block = blocks[i]; i++) {
        if (block.isShadow()) {
            block.setShadow(false);
            // Delete the shadow DOM attached to the block so that the shadow block
            // does not respawn. Dependent on implementation details.
            var parentConnection = block.outputConnection ?
                block.outputConnection.targetConnection :
                block.previousConnection.targetConnection;
            if (parentConnection) {
                parentConnection.setShadowDom(null);
            }
            this.model.addShadowBlock(block.id);
            this.view.markShadowBlock(block);
        }
    }
};

/**
 * Sets the currently selected mode that determines what the toolbox workspace
 * is being used to edit. Updates the view and then saves and loads XML
 * to and from the toolbox and updates the help text.
 * @param {string} tab The type of tab being switched to
 *    (WorkspaceFactoryController.MODE_TOOLBOX or
 *    WorkspaceFactoryController.MODE_PRELOAD).
 */
WorkspaceFactoryController.prototype.setMode = function (mode) {
    // No work to change mode that's currently set.
    if (this.selectedMode == mode) {
        return;
    }

    // No work to change mode that's currently set.
    if (this.selectedMode == mode) {
        return;
    }

    // Set tab selection and display appropriate tab.
    this.view.setModeSelection(mode);

    // Update selected tab.
    this.selectedMode = mode;

    // Update help text above workspace.
    this.view.updateHelpText(mode);

    if (mode == WorkspaceFactoryController.MODE_TOOLBOX) {
        // Open the toolbox editing space.
        this.model.savePreloadXml
            (Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
        this.clearAndLoadXml_(this.model.getSelectedXml());
        this.view.disableWorkspace(this.view.shouldDisableWorkspace
            (this.model.getSelected()));
    } else {
        // Open the pre-loaded workspace editing space.
        if (this.model.getSelected()) {
            this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
        }
        this.clearAndLoadXml_(this.model.getPreloadXml());
        this.view.disableWorkspace(false);
    }
};

/**
 * Clears the toolbox workspace and loads XML to it, marking shadow blocks
 * as necessary.
 * @private
 * @param {!Element} xml The XML to be loaded to the workspace.
 */
WorkspaceFactoryController.prototype.clearAndLoadXml_ = function (xml) {
    this.toolboxWorkspace.clear();
    this.toolboxWorkspace.clearUndo();
    Blockly.Xml.domToWorkspace(xml, this.toolboxWorkspace);
    this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace
        (this.toolboxWorkspace.getAllBlocks(false)));
    this.warnForUndefinedBlocks_();
};

/**
 * Sets the standard default options for the options object and updates
 * the preview workspace. The default values depends on if categories are
 * present.
 */
WorkspaceFactoryController.prototype.setStandardOptionsAndUpdate = function () {
    this.view.setBaseOptions();
    this.view.setCategoryOptions(this.model.hasElements());
    this.generateNewOptions();
};

/**
 * Generates a new options object for injecting a Blockly workspace based
 * on user input. Should be called every time a change has been made to
 * an input field. Updates the model and reinjects the preview workspace.
 */
WorkspaceFactoryController.prototype.generateNewOptions = function () {
    this.model.setOptions(this.readOptions_());

    this.reinjectPreview(Blockly.utils.toolbox.parseToolboxTree(
        this.generator.generateToolboxXml()));
};

/**
 * Generates a new options object for injecting a Blockly workspace based on
 * user input.
 * @return {!Object} Blockly injection options object.
 * @private
 */
WorkspaceFactoryController.prototype.readOptions_ = function () {
    var optionsObj = Object.create(null);

    // Add all standard options to the options object.
    // Use parse int to get numbers from value inputs.
    var readonly = document.getElementById('option_readOnly_checkbox').checked;
    if (readonly) {
        optionsObj['readOnly'] = true;
    } else {
        optionsObj['collapse'] =
            document.getElementById('option_collapse_checkbox').checked;
        optionsObj['comments'] =
            document.getElementById('option_comments_checkbox').checked;
        optionsObj['disable'] =
            document.getElementById('option_disable_checkbox').checked;
        if (document.getElementById('option_infiniteBlocks_checkbox').checked) {
            optionsObj['maxBlocks'] = Infinity;
        } else {
            var maxBlocksValue =
                document.getElementById('option_maxBlocks_number').value;
            optionsObj['maxBlocks'] = typeof maxBlocksValue == 'string' ?
                parseInt(maxBlocksValue) : maxBlocksValue;
        }
        optionsObj['trashcan'] =
            document.getElementById('option_trashcan_checkbox').checked;
        optionsObj['horizontalLayout'] =
            document.getElementById('option_horizontalLayout_checkbox').checked;
        optionsObj['toolboxPosition'] =
            document.getElementById('option_toolboxPosition_checkbox').checked ?
                'end' : 'start';
    }

    optionsObj['css'] = document.getElementById('option_css_checkbox').checked;
    optionsObj['media'] = document.getElementById('option_media_text').value;
    optionsObj['rtl'] = document.getElementById('option_rtl_checkbox').checked;
    optionsObj['scrollbars'] =
        document.getElementById('option_scrollbars_checkbox').checked;
    optionsObj['sounds'] =
        document.getElementById('option_sounds_checkbox').checked;
    optionsObj['oneBasedIndex'] =
        document.getElementById('option_oneBasedIndex_checkbox').checked;

    // If using a grid, add all grid options.
    if (document.getElementById('option_grid_checkbox').checked) {
        var grid = Object.create(null);
        var spacingValue =
            document.getElementById('gridOption_spacing_number').value;
        grid['spacing'] = typeof spacingValue == 'string' ?
            parseInt(spacingValue) : spacingValue;
        var lengthValue = document.getElementById('gridOption_length_number').value;
        grid['length'] = typeof lengthValue == 'string' ?
            parseInt(lengthValue) : lengthValue;
        grid['colour'] = document.getElementById('gridOption_colour_text').value;
        if (!readonly) {
            grid['snap'] =
                document.getElementById('gridOption_snap_checkbox').checked;
        }
        optionsObj['grid'] = grid;
    }

    // If using zoom, add all zoom options.
    if (document.getElementById('option_zoom_checkbox').checked) {
        var zoom = Object.create(null);
        zoom['controls'] =
            document.getElementById('zoomOption_controls_checkbox').checked;
        zoom['wheel'] =
            document.getElementById('zoomOption_wheel_checkbox').checked;
        var startScaleValue =
            document.getElementById('zoomOption_startScale_number').value;
        zoom['startScale'] = typeof startScaleValue == 'string' ?
            Number(startScaleValue) : startScaleValue;
        var maxScaleValue =
            document.getElementById('zoomOption_maxScale_number').value;
        zoom['maxScale'] = typeof maxScaleValue == 'string' ?
            Number(maxScaleValue) : maxScaleValue;
        var minScaleValue =
            document.getElementById('zoomOption_minScale_number').value;
        zoom['minScale'] = typeof minScaleValue == 'string' ?
            Number(minScaleValue) : minScaleValue;
        var scaleSpeedValue =
            document.getElementById('zoomOption_scaleSpeed_number').value;
        zoom['scaleSpeed'] = typeof scaleSpeedValue == 'string' ?
            Number(scaleSpeedValue) : scaleSpeedValue;
        optionsObj['zoom'] = zoom;
    }

    return optionsObj;
};

/**
 * Imports blocks from a file, generating a category in the toolbox workspace
 * to allow the user to use imported blocks in the toolbox and in pre-loaded
 * blocks.
 * @param {!File} file File object for the blocks to import.
 * @param {string} format The format of the file to import, either 'JSON' or
 *    'JavaScript'.
 */
WorkspaceFactoryController.prototype.importBlocks = function (file, format) {
    // Generate category name from file name.
    var categoryName = file.name;

    var controller = this;
    var reader = new FileReader();

    // To be executed when the reader has read the file.
    reader.onload = function () {
        try {
            // Define blocks using block types from file.
            var blockTypes = FactoryUtils.defineAndGetBlockTypes(reader.result,
                format);

            // If an imported block type is already defined, check if the user wants
            // to override the current block definition.
            if (controller.model.hasDefinedBlockTypes(blockTypes)) {
                var msg = '导入的积木与工具箱中已有的积木同名。\n您确定要覆盖当前定义的积木吗？';
                var continueAnyway = confirm(msg);
                BlocklyDevTools.Analytics.onWarning(msg);
                if (!continueAnyway) {
                    return;
                }
            }

            var blocks = controller.generator.getDefinedBlocks(blockTypes);
            // Generate category XML and append to toolbox.
            var categoryXml = FactoryUtils.generateCategoryXml(blocks, categoryName);
            // Get random color for category between 0 and 360. Gives each imported
            // category a different color.
            var randomColor = Math.floor(Math.random() * 360);
            categoryXml.setAttribute('colour', randomColor);
            controller.toolbox.appendChild(categoryXml);
            controller.toolboxWorkspace.updateToolbox(controller.toolbox);
            // Update imported block types.
            controller.model.addImportedBlockTypes(blockTypes);
            // Reload current category to possibly reflect any newly defined blocks.
            controller.clearAndLoadXml_
                (Blockly.Xml.workspaceToDom(controller.toolboxWorkspace));

            BlocklyDevTools.Analytics.onImport('BlockDefinitions' +
                (format == 'JSON' ? '.json' : '.js'));
        } catch (e) {
            msg = '无法从文件中读取积木。';
            alert(msg);
            BlocklyDevTools.Analytics.onError(msg);
            window.console.log(e);
        }
    }

    // Read the file asynchronously.
    reader.readAsText(file);
};

/**
 * Updates the block library category in the toolbox workspace toolbox.
 * @param {!Element} categoryXml XML for the block library category.
 * @param {!Array.<string>} libBlockTypes Array of block types from the block
 *    library.
 */
WorkspaceFactoryController.prototype.setBlockLibCategory =
    function (categoryXml, libBlockTypes) {
        var blockLibCategory = document.getElementById('blockLibCategory');

        // Set category ID so that it can be easily replaced, and set a standard,
        // arbitrary block library color.
        categoryXml.id = 'blockLibCategory';
        categoryXml.setAttribute('colour', 260);

        // Update the toolbox and toolboxWorkspace.
        this.toolbox.replaceChild(categoryXml, blockLibCategory);
        this.toolboxWorkspace.toolbox_.clearSelection();
        this.toolboxWorkspace.updateToolbox(this.toolbox);

        // Update the block library types.
        this.model.updateLibBlockTypes(libBlockTypes);

        // Reload XML on page to account for blocks now defined or undefined in block
        // library.
        this.clearAndLoadXml_(Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
    };

/**
 * Return the block types used in the custom toolbox and pre-loaded workspace.
 * @return {!Array.<string>} Block types used in the custom toolbox and
 *    pre-loaded workspace.
 */
WorkspaceFactoryController.prototype.getAllUsedBlockTypes = function () {
    return this.model.getAllUsedBlockTypes();
};

/**
 * Determines if a block loaded in the workspace has a definition (if it
 * is a standard block, is defined in the block library, or has a definition
 * imported).
 * @param {!Blockly.Block} block The block to examine.
 */
WorkspaceFactoryController.prototype.isDefinedBlock = function (block) {
    return this.model.isDefinedBlockType(block.type);
};

/**
 * Sets a warning on blocks loaded to the workspace that are not defined.
 * @private
 */
WorkspaceFactoryController.prototype.warnForUndefinedBlocks_ = function () {
    var blocks = this.toolboxWorkspace.getAllBlocks(false);
    for (var i = 0, block; block = blocks[i]; i++) {
        if (!this.isDefinedBlock(block)) {
            block.setWarningText(block.type + ' 未定义（它不是“我的积木”中的标准积木，也不是导入的积木）');
        }
    }
};

/**
 * Determines if a standard variable category is in the custom toolbox.
 * @return {boolean} True if a variables category is in use, false otherwise.
 */
WorkspaceFactoryController.prototype.hasVariablesCategory = function () {
    return this.model.hasVariables();
};

/**
 * Determines if a standard procedures category is in the custom toolbox.
 * @return {boolean} True if a procedures category is in use, false otherwise.
 */
WorkspaceFactoryController.prototype.hasProceduresCategory = function () {
    return this.model.hasProcedures();
};

/**
 * Determines if there are any unsaved changes in workspace factory.
 * @return {boolean} True if there are unsaved changes, false otherwise.
 */
WorkspaceFactoryController.prototype.hasUnsavedChanges = function () {
    return this.hasUnsavedToolboxChanges || this.hasUnsavedPreloadChanges;
};
